﻿using Devil;
using GameToolkit.BehaviourTree;
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

namespace GameToolkit.Editor
{
    public interface IStoryNodeVisual
    {
        TaskGraphAsset.NodeAsset nodeAsset { get; }
        Vector2 position { get; }
        void Validate();
    }

    public class StoryNodeVisual : Node, IStoryNodeVisual
    {
        TaskNode mNodeAsset;
        public TaskGraphAsset.NodeAsset nodeAsset => mNodeAsset;

        protected Label mDescript;
        protected VisualElement mContentContainer;
        protected VisualElement mIcon;
        public VisualElement btContainer => mContentContainer;
        public Vector2 position => GetPosition().position;
        public Port startPort { get; private set; }
        public Port killPort { get; private set; }

        //public Port exitPort { get; private set; }
        //public Port successPort { get; private set; }
        //public Port failPort { get; private set; }

        TaskEntity mBindEntity;
        int mState;
        public Port GetExitPort(int id)
        {
            for (int i = outputContainer.childCount - 1; i >= 0; i--)
            {
                var port = outputContainer[i] as ISingleConnectionBetweenNodes;
                if (port != null && port.Id == id)
                    return (Port)port;
            }
            return null;
        }

        public StoryNodeVisual(StoryNodeLib.StoryNodeInfo info, TaskNode node)
        {
            if (string.IsNullOrEmpty(node._guid))
                node._guid = GUID.Generate().ToString();
            viewDataKey = node._guid;
            mNodeAsset = node;
            var icon = new VisualElement();
            icon.name = "icon";
            icon.AddToClassList("node-icon");
            titleContainer.Insert(0, icon);
            var icontex = info.Icon;
            if (icontex != null)
                icon.style.backgroundImage = icontex;
            mIcon = icon;

            mContentContainer = new VisualElement();
            mContentContainer.name = "node-container";
            mContentContainer.AddToClassList("node-contents");
            mainContainer.Add(mContentContainer);

            var desc = new Label();
            desc.AddToClassList("node-descript");
            btContainer.Add(desc);
            mDescript = desc;

            //var guidLab = new Label();
            //guidLab.name = "guid";
            //guidLab.text = node._guid;
            //mContentContainer.Add(guidLab);

            var isFinalTask = node is FinalTaskNode;
            startPort = InstantiatePort(Orientation.Horizontal, Direction.Input, isFinalTask ? Port.Capacity.Single : Port.Capacity.Multi, typeof(TaskNode));
            startPort.portName = isFinalTask ? "Terminate" : "Ready";
            startPort.portColor = Color.white;
            inputContainer.Add(startPort);

            if (!isFinalTask)
            {
                if (!info.IsHiddenGiveUp)
                {
                    killPort = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(TaskNode));
                    killPort.portName = "Give Up";
                    killPort.portColor = Color.red;
                    inputContainer.Add(killPort);
                }

                //conditionPort = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(NodeState));
                //conditionPort.portName = "Condition?";
                //conditionPort.portColor = new Color(0.4f, 0.4f, 0.1f);
                //btContainer.Add(conditionPort);

                //exitPort = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(TaskNode));
                //exitPort.portName = "Exit";
                //exitPort.portColor = Color.white;
                //((ISingleConnectionBetweenNodes)exitPort).Id = TaskNode.RESULT_FINISH;
                //outputContainer.Add(exitPort);

                if (!info.IsReservedAlways)
                {
                    var port = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(TaskNode));
                    port.portName = "Always";
                    port.portColor = Color.white;
                    ((ISingleConnectionBetweenNodes)port).Id = (int)EReservedTaskResult.Always;
                    outputContainer.Add(port);
                }
                if (!info.IsReservedFinish)
                {
                    var port = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(TaskNode));
                    port.portName = "Finish";
                    port.portColor = Color.green;
                    ((ISingleConnectionBetweenNodes)port).Id = (int)EReservedTaskResult.Finish;
                    outputContainer.Add(port);
                }
                if (!info.IsReservedGiveUp)
                {
                    var port = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(TaskNode));
                    port.portName = "Give Up";
                    port.portColor = Color.red;
                    ((ISingleConnectionBetweenNodes)port).Id = (int)EReservedTaskResult.GiveUp;
                    outputContainer.Add(port);
                }
                for (int i = 0; i < info.Results.Length; i++)
                {
                    var port = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(TaskNode));
                    port.portName = info.Results[i].PortName;
                    port.portColor = info.Results[i].PortColor;
                    ((ISingleConnectionBetweenNodes)port).Id = info.Results[i].Value;
                    outputContainer.Add(port);
                }


                //var abortPort = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(TaskNode));
                //abortPort.portName = "Give Up";
                //abortPort.portColor = Color.red;
                //((ISingleConnectionBetweenNodes)abortPort).Id = (int)EReservedTaskResult.GiveUp;
                //outputContainer.Add(abortPort);

                //successPort = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(TaskNode));
                //successPort.portName = "Success";
                //successPort.portColor = Color.green;
                //outputContainer.Add(successPort);

                //failPort = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(TaskNode));
                //failPort.portName = "Failed";
                //failPort.portColor = Color.red;
                //outputContainer.Add(failPort);
            }
            else if (startPort is GraphPort<Edge> gp)
                gp.SyncOutputColor = true;

            capabilities &= ~Capabilities.Collapsible;
            capabilities |= Capabilities.Groupable;

            title = info.Title;
            Validate();
            //mDescript.text = StoryNodeLib.GetDescription(node);

            SetPosition(new Rect(node.m_Position, Vector2.zero));
        }

        public void BindEntity(TaskEntity entity)
        {
            if(mBindEntity != entity)
            {
                if (mBindEntity != null)
                    mBindEntity.OnStateChanged -= OnStateChanged;
                mBindEntity = entity;
                if (mBindEntity != null)
                    mBindEntity.OnStateChanged += OnStateChanged;
                OnStateChanged(entity);
            }
        }

        private void OnStateChanged(TaskEntity entity)
        {
            if (!ParallelUnity.IsMainThread)
                MainThread.RunOnMainThread(Validate);
            else
                Validate();

        }

        Port GetResultPort(int id)
        {
            for (int i = 0; i < outputContainer.childCount; i++)
            {
                var port = outputContainer[i] as ISingleConnectionBetweenNodes;
                if (port != null && port.Id == id)
                    return (Port)port;
            }
            return null;
        }

        bool ContainsPort(List<TaskResultDescripter> ports, int id)
        {
            if (id == (int)EReservedTaskResult.Always || id == (int)EReservedTaskResult.GiveUp || id == (int)EReservedTaskResult.Finish)
                return true;
            for (int i = 0; i < ports.Count; i++)
            {
                if (ports[i].value == id)
                    return true;
            }
            return false;
        }

        public void Release()
        {
            BindEntity(null);
            mNodeAsset = null;
        }

        public void Validate()
        {
            if (mNodeAsset == null)
                return;
            var stat = mBindEntity == null ? 0 : (int)mBindEntity.State;
            if (stat == (int)ETaskState.Complete && mBindEntity.ResultValue == (int)EReservedTaskResult.GiveUp)
                stat = (int)NodeState.Failed;
            if (stat != mState)
            {
                var cname = ExecutableNodeVisual.STAT_CLASS[mState];
                if (!string.IsNullOrEmpty(cname))
                    titleContainer.RemoveFromClassList(cname);
                mState = stat;
                cname = ExecutableNodeVisual.STAT_CLASS[mState];
                if (!string.IsNullOrEmpty(cname))
                    titleContainer.AddToClassList(cname);
                //startPort.portColor = ExecutableNodeVisual.STAT_COLOR[(int)mState];
                //if (actionPort != null)
                //{
                //    actionPort.portColor = STAT_COLOR[(int)mState];
                //}
            }

            var desc = StoryNodeLib.GetDescription(mNodeAsset);
            if(stat == (int)ETaskState.Complete || stat == (int)NodeState.Failed)
            {
                if (mBindEntity.ResultValue == (int)EReservedTaskResult.Finish || mBindEntity.ResultValue == (int)EReservedTaskResult.GiveUp)
                    desc = $"{desc}\n ({(EReservedTaskResult)mBindEntity.ResultValue})";
                else
                    desc = $"{desc}\n ({mBindEntity.ResultValue})";
            }
            mDescript.text = desc;
            if (mNodeAsset is IDynamicTaskResult dynamicResult)
            {
                var lst = CachedList<TaskResultDescripter>.GetList();
                for (int i = 0; i < dynamicResult.ResultCount; i++)
                {
                    var item = dynamicResult.GetResultDescripter(i);
                    if (string.IsNullOrEmpty(item.name))
                        item.name = $"#{item.value}";
                    lst.Add(item);
                }
                var graph = GetFirstAncestorOfType<GraphView>();
                for (int i = outputContainer.childCount - 1; i >= 0; i--)
                {
                    var port = outputContainer[i] as ISingleConnectionBetweenNodes;
                    if (port != null && !ContainsPort(lst, port.Id))
                    {
                        var item = (Port)port;
                        if (graph != null)
                        {
                            foreach (var conn in item.connections)
                            {
                                graph.RemoveElement(conn);
                            }
                        }
                        item.DisconnectAll();
                        outputContainer.RemoveAt(i);
                    }
                }
                for (int i = lst.Count - 1; i >= 0; i--)
                {
                    var port = GetResultPort(lst[i].value);
                    if (port != null)
                    {
                        port.portColor = lst[i].color;
                        port.portName = lst[i].name;
                        lst.RemoveAt(i);
                    }
                }
                for (int i = 0; i < lst.Count; i++)
                {
                    var port = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(TaskNode));
                    port.portName = lst[i].name;
                    port.portColor = lst[i].color;
                    ((ISingleConnectionBetweenNodes)port).Id = lst[i].value;
                    outputContainer.Add(port);
                }
                CachedList<TaskResultDescripter>.Release(lst);
            }
        }

        public override Port InstantiatePort(Orientation orientation, Direction direction, Port.Capacity capacity, System.Type type)
        {
            var port = new GraphPort<Edge>(orientation, direction, capacity, type);
            if (type == typeof(TaskNode))
                port.AutoCreationFilter = StoryNodeLib.NO_FILTER;
            port.IsSingleConnection = true;
            return port;
            //return Port.Create<LinkEdge>(orientation, direction, capacity, type);
        }

        public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
        {
            evt.menu.AppendAction("Refresh", (dt) => Validate(), DropdownMenuAction.Status.Normal);

            Dictionary<string, object[]> menus = new Dictionary<string, object[]>();
            GetContextMenuInfo(mNodeAsset, mBindEntity, "", menus);
            foreach (var kv in menus)
            {
                var menu = kv.Value;
                evt.menu.AppendAction(menu[cid_name].ToString(), ExecuteContextMenu, GetContextMenuState, menu);
            }
            evt.menu.AppendSeparator();
            base.BuildContextualMenu(evt);
        }

        // target, cmenu, method, validate
        readonly int cid_composite = 0;
        readonly int cid_runtime = 1;
        readonly int cid_name = 2;
        readonly int cid_method = 3;
        readonly int cid_validte = 4;

        void GetContextMenuInfo(TaskNode compositeInst, TaskEntity runtimeInst, string prefix, Dictionary<string, object[]> menus)
        {
            var flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.InvokeMethod;
            var tp = compositeInst.GetType();
            var baseType = typeof(Composite);
            while (tp != null && tp != baseType)
            {
                var methods = tp.GetMethods(flags);
                tp = tp.BaseType;
                foreach (var method in methods)
                {
                    var menuItems = method.GetCustomAttributes(true);
                    var len = menuItems == null ? 0 : menuItems.Length;
                    for (int i = 0; i < menuItems.Length; i++)
                    {
                        if (menuItems[i] is ContextMenu cmenu)
                        {
                            object[] menu;
                            if (!menus.TryGetValue(cmenu.menuItem, out menu))
                            {
                                menu = new object[5];
                                menus.Add(cmenu.menuItem, menu);
                                menu[cid_composite] = compositeInst;
                                menu[cid_runtime] = runtimeInst;
                                if (string.IsNullOrEmpty(prefix))
                                    menu[cid_name] = cmenu.menuItem;
                                else
                                    menu[cid_name] = $"{prefix}/{cmenu.menuItem}";
                            }
                            menu[cmenu.validate ? cid_validte : cid_method] = method;
                        }
                    }
                }
            }
        }

        bool InvokeContextMenuValidater(MethodInfo method, object target, object runtimeInstance)
        {
            try
            {
                var arg = method.GetParameters();
                if (arg == null || arg.Length == 0)
                    return (bool)method.Invoke(target, new object[0]);
                else
                    return (bool)method.Invoke(target, new object[] { runtimeInstance });
            }
            catch (System.Exception e)
            {
                Debug.LogError($"\"{method.DeclaringType.Name}.{method.Name}\" is not correct as ContextMenu validater.\n\tdefination: bool MethodName() or bool MethodName(runtimeType runtimeInst)\n\n{e}");
                return false;
            }
        }

        private DropdownMenuAction.Status GetContextMenuState(DropdownMenuAction action)
        {
            bool valid = true;
            var menu = action.userData as object[];
            if (menu[4] is MethodInfo method)
            {
                valid = InvokeContextMenuValidater(method, menu[cid_composite], menu[cid_runtime]);
            }
            return valid ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled;
        }

        private void ExecuteContextMenu(DropdownMenuAction action)
        {
            var menu = action.userData as object[];
            if (menu[cid_method] is MethodInfo method)
            {
                try
                {
                    var arg = method.GetParameters();
                    if (arg == null || arg.Length == 0)
                        method.Invoke(menu[cid_composite], new object[0]);
                    else
                        method.Invoke(menu[cid_composite], new object[] { menu[cid_runtime] });
                }
                catch (System.Exception e)
                {
                    Debug.LogError($"\"{method.DeclaringType.Name}.{method.Name}\" is not correct as ContextMenu.\n\tdefination: void MethodName() or void MethodName(runtimeType runtimeInst)\n\n{e}");
                }
            }
        }
    }
}
